Skip to main content

11 延迟解析:V8是如何实现闭包的

V8 执行 JavaScript 代码总流程

在编译 JavaScript 代码的过程中,V8 并不会一次性将所有的 JavaScript 解析为中间代码:

  1. 会增加编译时间,这会严重影响到首次执行 JavaScript 代码的速度,让用户感觉到卡顿;
  2. 解析完成的字节码和编译之后的机器代码都会存放在内存中,一次性解析和编译所有 JavaScript 代码,这些中间代码和机器代码将会一直占用内存

主流的 JavaScript 虚拟机都实现了惰性解析:指解析器在解析的过程中,如果遇到函数声明,会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。

惰性解析的过程

function foo(a, b) {
var d = 10;
var f = 10;
return d + f + a + b;
}
var a = 1;
var c = 4;
foo(1, 5);

V8 会至上而下解析这段代码,在解析过程中遇到 foo 函数,只是一个函数声明语句,V8 在这个阶段只需要将该函数转换为函数对象:

这里只是将该函数声明转换为函数对象,并没有解析和编译函数内部的代码,也不会为 foo 函数的内部代码生成抽象语法树。继续往下解析,最终生成抽象语法树结果:

代码解析完成之后,V8 便会按照顺序自上而下执行代码,首先会先执行“a=1”和“c=4”这两个赋值表达式,接下来执行 foo 函数的调用,过程是从 foo 函数对象中取出函数代码,然后和编译顶层代码一样,V8 会先编译 foo 函数的代码,编译时同样需要先将其编译为抽象语法树和字节码,然后再解释执行。

在 V8 实现惰性解析的过程中,需要支持 JavaScript 中的闭包特性。

拆解闭包——JavaScript 的三个特性

允许在函数内部定义新的函数

function foo() {
function inner() {}
inner();
}

可以在内部函数中访问父函数中定义的变量

var d = 20;
//inner函数的父函数,词法作用域
function foo() {
var d = 55;
//foo的内部函数
function inner() {
return d + 2;
}
inner();
}

因为函数是一等公民,所以函数可以作为返回值

function foo() {
return function inner(a, b) {
const c = a + b;
return c;
};
}
const f = foo();

闭包给惰性解析带来的问题

function foo() {
var d = 20;
return function inner(a, b) {
const c = a + b + d;
return c;
};
}
const f = foo();

代码的执行过程:

  1. 当调用 foo 函数时,foo 函数会将它的内部函数 inner 返回给全局变量 f;
  2. 然后 foo 函数执行结束,执行上下文被 V8 销毁;
  3. 虽然 foo 函数的执行上下文被销毁了,但是依然存活的 inner 函数引用了 foo 函数作用域中的变量 d。

带来的问题:

  1. 当 foo 执行结束时,变量 d 该不该被销毁?如果不应该被销毁,那么应该采用什么策略?
  2. 如果采用了惰性解析,那么当执行到 foo 函数时,V8 只会解析 foo 函数,并不会解析内部的 inner 函数,那么这时候 V8 就不知道 inner 函数中是否引用了 foo 函数的变量 d。

当执行 foo 函数的时候,堆栈的变化:

foo 函数的执行上下文虽然被销毁了,但是 inner 函数引用的 foo 函数中的变量却不能被销毁,那么 V8 就需要为这种情况做特殊处理,需要保证即便 foo 函数执行结束,但是 foo 函数中的 d 变量依然保持在内存中,不能随着 foo 函数的执行上下文被销毁掉。

在执行 foo 函数的阶段,虽然采取了惰性解析,不会解析和执行 foo 函数中的 inner 函数,但是 V8 还是需要判断 inner 函数是否引用了 foo 函数中的变量,负责处理这个任务的模块叫做预解析器。

预解析器如何解决闭包所带来的问题

V8 引入预解析器,比如当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该函数做一次快速的预解析:

  • 判断当前函数是不是存在一些语法上的错误,发现了语法错误,会向 V8 抛出语法错误;
  • 检查函数内部是否引用了外部变量,,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用。